﻿using System;

using System.Collections.Generic;
using Silvermoon.OpenGL.Native;
using System.Drawing;
using Silvermoon.OpenGL;
using Silvermoon.Controls;
using Silvermoon.Shapes;
using Silvermoon.Utils;
using Silvermoon.Controls.Interfaces;
using Silvermoon.Controls.Classes;
using System.Diagnostics;
using Silvermoon.Controls.Base;

namespace Silvermoon.Core
{
    /// <summary>
    /// Renders Controls, Shapes and Elements using OpenGL.
    /// </summary>
    public class Renderer
    {
        #region structs

        public struct RenderData
        {
            public bool DisableClipping;
            public float X;
            public float Y;
            public float ScaleX;
            public float ScaleY;
            public int Alpha;
            public Color TextColor;
            public Rectangle ScissorBox;

            public Rectangle Translate(Rectangle r)
            {
                float x = X + r.X * ScaleX + 0.5f;
                float y = Y + r.Y * ScaleY + 0.5f;
                float w = r.Width * ScaleX + 0.5f;
                float h = r.Height * ScaleY + 0.5f;
                return new Rectangle((int)x, (int)y, (int)w, (int)h);
            }

            internal Rectangle GetBounds()
            {
                Rectangle rect = ScissorBox;
                rect.Offset((int)X, (int)Y);
                return rect;
            }
        }

        #endregion
        #region fields

        public readonly Color StartTextColor = Color.White;

        private bool isStencilBufferPrepared;
        private bool scale;
        private Rectangle scissorBox;
        private Color backgroundColor = Color.Black;
        protected Device device { get; private set; }

        #endregion
        #region ctors/dtors

        internal Renderer(Device device, Rectangle clientRectangle)
        {
            if (device == null) throw new ArgumentNullException("device");

            Perspective = true;
            this.ClientRectangle = clientRectangle;
            Invalidated = true;
            this.device = device;
            Init();
        }

        #endregion
        #region props

        /// <summary>
        /// Gets or sets a hint whether a shape is intendend to be drawn shadowed.
        /// </summary>
        public bool Shadowed { get; set; }

        /// <summary>
        /// Gets the color that is the current color to render text or drawings.
        /// </summary>
        public Color TextColor { get; private set; }

        /// <summaTery>
        /// Gets the current alpha value.
        /// </summary>
        public int Alpha { get; private set; }

        /// <summary>
        /// Specifies how controls are actually scaled.
        /// Note that this does not reflect shapes since they are already scaled.
        /// </summary>
        protected SizeF ScaleFactor { get; private set; }


        public Device Device { get { return device; } internal set { device = value; } }
        public bool Perspective { get; set; }
        public bool EnableScissor = true;
        public bool Invalidated { get; set; }
        protected bool Dirty;

        public Rectangle ClientRectangle { get; private set; }

        /// <summary>
        /// Gets whether the renderer is currently rendering.
        /// </summary>
        public bool IsRendering { get; private set; }

        protected Rectangle ScreenBounds { get; private set; }

        public Color BackgroundColor
        {
            get { return backgroundColor; }
            set
            {
                backgroundColor = value;
                device.ClearColor(value);
            }
        }

        public Rectangle ScissorBox
        {
            get { return scissorBox; }
            set
            {
                if (scissorBox != value)
                {
                    scissorBox = value;
                    int cx = value.X;
                    int cy = ScreenBounds.Height - (value.Bottom);
                    gl.Scissor(cx, cy, value.Width, value.Height);
                }
            }
        }

        #endregion
        #region methods

        private void Init()
        {
            int v = gl.GetIntegerValue(ParameterName.StencilBits);

            gl.ClearDepthf(9.0f);
            gl.Disable(Capability.DepthTest);
            gl.Disable(Capability.AlphaTest);
            gl.Disable(Capability.Normalize);
            gl.Enable(Capability.Blend);
            gl.Enable(Capability.Dither);
            gl.Disable(Capability.CullFace);
            if (EnableScissor) gl.Enable(Capability.ScissorTest); else gl.Disable(Capability.ScissorTest);
            gl.DisableClientState(ClientStateArray.Color);

            gl.AlphaFunc(Comparison.NotEqual, 0.0f);
            gl.BlendFunc(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha);
            gl.DepthFunc(Comparison.Never);
            gl.Hint(HintTarget.PerspectiveCorrection, HintMode.Fastest);
            gl.ShadeModel(ShadeModel.Smoth);
        }


        public void Render(ControlCollection controls)
        {
            if (IsRendering)
            {
                return;
            }
            IsRendering = true;
            try
            {
                do
                {
                    Dirty = false;
                    Rectangle bounds = InitializeRender();
                    BeginRender();
                    Invalidated = false;

                    if (controls != null) RenderControls(controls);
                } while (Dirty);

                // if one or more textures are captured during rendering, the canvas is invalid, and thus should not be presented:
                device.Present();
            }
#if DEBUG
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
                HandleException(ex);
            }
#endif
            finally
            {
                IsRendering = false;
            }
        }

        [Conditional("DEBUG")]
        private void HandleException(Exception ex)
        {
            throw new RenderException("", ex);
        }

        private void RenderCaptured(ControlCollection controls)
        {
            IsRendering = true;
            try
            {
                Rectangle bounds = InitializeRender();
                BeginRender();

                if (controls != null) RenderControls(controls);

                Invalidated = false;
            }
            finally
            {
                IsRendering = false;
            }
        }


        /// <summary>
        /// Gets or sets the RenderData which is currently used.
        /// RenderData describes the limits of the boundaries and the tranforming factors to apply.
        /// </summary>
        protected RenderData CurrentRenderData;

        public float ScaleX { get { return CurrentRenderData.ScaleX; } }
        public float ScaleY { get { return CurrentRenderData.ScaleY; } }


        private Rectangle InitializeRender()
        {
            scale = Screen.ControlScaleFactor.Width != 1f;
            ScaleFactor = Screen.ControlScaleFactor;
            Rectangle bounds = ClientRectangle;


            RenderData startData = new RenderData
            {
                DisableClipping = false,
                ScaleX = 1f,
                ScaleY = 1f,
                Alpha = 255,
                TextColor = StartTextColor,
                ScissorBox = bounds
            };
            CurrentRenderData = startData;

            this.ScreenBounds = bounds;
            SetProjectionMatrix();
            //SetProjectionMatrix(ClientRectangle.Width, ClientRectangle.Height, 60F, 0.2F, 2000F);
            //egl.WaitNative(egl.EGL_CORE_NATIVE_ENGINE);
            return bounds;
        }

        /// <summary>
        /// Renders an enumeration of controls.
        /// </summary>
        /// <param name="controls">Controls to render.</param>
        public void RenderControls(IEnumerable<Control> controls)
        {
            foreach (Control c in controls)
            {
                if (c.Visible && c.Opacity >= 2)
                {
                    RenderControl(c);
                }
                else
                {
                    c.StartAnimations();
                }
            }
        }

        public void RenderControls(ControlCollection controls)
        {
            Control first = controls.First;
            Control c = first;
            while (c != null)
            {
                if (c.Visible && c.Opacity >= 2) RenderControl(c); else c.StartAnimations();
                c = c.NextControl;
                ValidateNext(c, first);
            }
        }




        [Conditional("DEBUG2")]
        private void ValidateNext(Control c, Control first)
        {
            if (c == first || (c != null || c.NextControl == c))
            {
                throw new ArgumentOutOfRangeException("", "Circular list discovered.");
            }
        }

        /// <summary>
        /// Occurs before rendering all controls.
        /// </summary>
        private void BeginRender()
        {
            Rectangle bounds = ScreenBounds;

            if (EnableScissor)
            {
                gl.Enable(Capability.ScissorTest);
                ScissorBox = new Rectangle(0, 0, bounds.Width, bounds.Height);
            }
            else
            {
                gl.Disable(Capability.ScissorTest);
            }

            gl.MatrixMode(MatrixMode.ModelView);
            gl.LoadIdentity();

            isStencilBufferPrepared = false;
            if (!this.BackgroundColor.IsEmpty) gl.Clear(ColorBufferBit.Color);
            gl.ColorMask(1, 1, 1, 1);
            gl.Color4f(1f, 1f, 1f, 1f);
            gl.ShadeModel(ShadeModel.Smoth);
            gl.EnableClientState(ClientStateArray.Vertex);
            gl.ActiveTexture(TextureUnit.Texture0);
            gl.ClientActiveTexture(TextureUnit.Texture0);
            gl.EnableClientState(ClientStateArray.TextureCoord);
            gl.Enable(Capability.Texture2D);
        }



        private void SetProjectionMatrix()
        {
            Rectangle bounds = ClientRectangle;
            gl.MatrixMode(MatrixMode.Projection);
            gl.LoadIdentity();
            if (Perspective)
            {
                // note: since top = tan(fov)*nearClip => fov =  atn(top/nearClip) = atn(h/(200*nearClip)
                // hence fov = 87.1° for h=800 and 85.2° for h=480.
                const float div = 200f;
                gl.Frustumf(-bounds.Width / div, bounds.Width / div, bounds.Height / div, -bounds.Height / div, 0.2F, 50f);
                gl.Translatef(-(float)bounds.Width / 2f, -(float)bounds.Height / 2f, -20f);
                gl.Scalef(1f, 1f, 0.012f);
            }
            else
            {
                gl.Orthof(0, bounds.Width, bounds.Height, 0F, -100F, 100F);
            }
        }

        private void SetProjectionMatrix(float screenWidth, float screenHeight, float fov, float nearClip, float farClip)
        {
            float aspect = screenWidth / screenHeight;
            float top = -(float)Math.Tan(fov * Math.PI / 360D) * nearClip;
            float bottom = -top;
            float left = aspect * top;
            float right = -left;
            gl.MatrixMode(MatrixMode.Projection);
            gl.LoadIdentity();
            gl.Frustumf(left, right, bottom, top, nearClip, farClip);
            gl.Translatef(-screenWidth / 2f, -screenHeight / 2f, -690F);
            gl.MatrixMode(MatrixMode.ModelView);
        }

        /// <summary>
        /// Prepares the stencil buffer and clears it's data if not already done.
        /// </summary>
        public void PrepareStencilBuffer()
        {
            if (!isStencilBufferPrepared)
            {
                isStencilBufferPrepared = true;
                gl.Enable(Capability.StencilTest);
                gl.ClearStencil(0);
                gl.Clear(ColorBufferBit.Stencil);
            }
        }

        /// <summary>
        /// Renders a control
        /// </summary>
        /// <param name="control">Control to render.</param>        
        public void RenderControl(Control control)
        {
            if (control.IsDisposing) return;
            //Debug.WriteLine("Begin RenderControl " + control.Id);
#if DEBUG
            try
            {
#endif
            RenderData saved = CurrentRenderData;
            gl.PushMatrix();
#if DEBUG
            try
            {
#endif
            control.PreLoad();
#if DEBUG
            }
            catch (Exception ex)
            {
                throw new RenderException("RenderControl.PreLoad", ex);
            }
#endif

            control.StartAnimations();
            CurrentRenderData.X += control.X * CurrentRenderData.ScaleX;
            CurrentRenderData.Y += control.Y * CurrentRenderData.ScaleY;
            gl.Translatef(control.X, control.Y, 0f);

            Transformation t = control.Transformation;
            if (t != null)
            {
                Size size = control.Size;
                t.Apply(size, ref CurrentRenderData);
            }

            Rectangle bounds = new Rectangle(0, 0, control.Width, control.Height);
            Rectangle scissorBox = RectangleUtil.Cut(CurrentRenderData.Translate(bounds), CurrentRenderData.ScissorBox);
            CurrentRenderData.ScissorBox = scissorBox;
            bool clippingDisabled = CurrentRenderData.DisableClipping;
            if (clippingDisabled) gl.Disable(Capability.ScissorTest); else gl.Enable(Capability.ScissorTest);

            if (clippingDisabled || (scissorBox.Width > 0 && scissorBox.Height > 0))
            {
                ScissorBox = scissorBox;
                int alpha = CurrentRenderData.Alpha * control.Opacity / 255;
                CurrentRenderData.Alpha = alpha;

                Color textColor = control.TextColor;
                if (!textColor.IsEmpty)
                {
                    CurrentRenderData.TextColor = textColor;
                }
                else
                {
                    textColor = CurrentRenderData.TextColor;
                }
                TextColor = textColor;
                Alpha = alpha;

                // the control is now responsible for rendering it's shapes and child controls.
                // this allows to override how a controls renders.

#if DEBUG
                try
                {
#endif
                control.OnRender(this);
#if DEBUG
                }
                catch (Exception ex)
                {
                    throw new RenderException("RenderControl.Render", ex);
                }
#endif
            }
            CurrentRenderData = saved;
            gl.PopMatrix();
#if DEBUG
            }
            catch (Exception ex)
            {
                throw new RenderException("RenderControl " + control.GetType().Name + " " + control.GetHashCode().ToString(), ex);
            }
#endif
        }

        /// <summary>
        /// Renders an enumeration of shapes.
        /// </summary>
        /// <param name="shapes">Shapes to render.</param>
        public void RenderShapes(ShapeCollection shapes)
        {
#if DEBUG
            try
            {
#endif
            int alpha = Alpha;
            Shape shape = shapes.First;
            while (shape != null)
            {
                if (shape.Visible && shape.Opacity > 3) RenderShape(shape, alpha);
                shape = shape.NextShape;
            }
#if DEBUG
            }
            catch (Exception ex)
            {
                throw new RenderException("RenderShapes1", ex);
            }
#endif
        }

        /// <summary>
        /// Renders an enumeration of shapes.
        /// </summary>
        /// <param name="shapes">Shapes to render.</param>
        public void RenderShapes(IEnumerable<Shape> shapes)
        {
#if DEBUG
            try
            {
#endif
            int alpha = Alpha;
            foreach (Shape shape in shapes)
            {
                if (shape.Visible) RenderShape(shape, alpha);
            }
#if DEBUG
            }
            catch (Exception ex)
            {
                throw new RenderException("RenderShapes2", ex);
            }
#endif
        }


        /// <summary>
        /// Renders a shape.
        /// </summary>
        /// <param name="shape">The shape to render.</param>
        /// <param name="alpha">The alpha value for the view that specifies if the shape is transparent or opaque. Acceptable values are between 0 and 255.</param>
        public void RenderShape(Shape shape, int alpha)
        {
#if DEBUG
            try
            {
#endif
            if (!shape.Visible) return;
            shape.PreLoad();
            gl.PushMatrix();
            int x = shape.X;
            int y = shape.Y;

            gl.Translatef((float)x, (float)y, 0f);
            if (shape.Transformations != null)
            {
                RenderData data = new RenderData { Alpha = alpha };
                Size size = shape.Bounds.Size;
                foreach (Transformation t in shape.Transformations) t.Apply(size, ref data);
                alpha = data.Alpha;
            }
            gl.DisableClientState(ClientStateArray.Color);
            Alpha = alpha * shape.Opacity / 255;

            shape.Render(this);
            gl.PopMatrix();
#if DEBUG
            }
            catch (Exception ex)
            {
                throw new RenderException("RenderShape", ex);
            }
#endif
        }

        public void SetColor(Color c, int alpha)
        {
            int r = c.R;
            int g = c.G;
            int b = c.B;
            int a = (c.A * alpha) / 255;
            gl.Color4x(r << 8, g << 8, b << 8, a << 8);
        }

        public void ChangeSize(Size size)
        {
            this.ClientRectangle = new Rectangle(0, 0, size.Width, size.Height);
            Init();
            Invalidated = true;
        }

        private Texture Capture(Control c, int x, int y)
        {
            Size s = c.Size;
            if (s.Width <= 0 || s.Height <= 0) return null;
            Texture texture = new Texture();
            Rectangle clip = new Rectangle(x, y, s.Width, s.Height);
            clip.Y = ClientRectangle.Height - clip.Bottom;
            try
            {
                texture.Capture(clip);
            }
            catch (OpenGlException)
            {
                texture.Dispose();
                texture = null;
            }
            return texture;
        }

        internal Texture Capture(Control c)
        {
            return Capture(c, Color.Transparent);
        }

        /// <summary>
        /// Captures the complete screen.
        /// </summary>
        /// <returns>Texture that contains the screen image.</returns>
        public Texture Capture(ControlCollection controls)
        {
            Invalidated = true;
            RenderCaptured(controls);
            Texture texture = new Texture();
            Rectangle clip = new Rectangle(0, 0, ClientRectangle.Width, ClientRectangle.Height);
            clip.Y = ClientRectangle.Height - clip.Bottom;
            try
            {
                texture.Capture(clip);
            }
            catch (OpenGlException)
            {
                texture.Dispose();
                texture = null;
            }
            Invalidated = true;
            return texture;
        }

        /// <summary>
        /// Captures the specified control and returns a texture that contains the complete image of the control.
        /// Note that Capture ignores Transformations and Alpha value.
        /// </summary>
        /// <param name="c">The control to capture.</param>
        /// <returns>A texture that contains the image of the specified control, otherwise null.</returns>
        public Texture Capture(Control c, Color backgroundColor)
        {
            if (c.Width > 0 && c.Height > 0)
            {
                InitializeRender();
                BeginRender();
                Point location = c.Location;
                TextColor = Screen.Current.GetTextColor(c);
                int alpha = c.Opacity;
                Transformation transformation = c.Transformation;
                // c.Location = Point.Empty;
                c.Transformation = null;
                c.Opacity = 255;
                gl.Clear(ColorBufferBit.Color);

                BackgroundColor = backgroundColor;
                //            gl.ClearColor(0, 0, 0, 0);
                gl.ColorMask(1, 1, 1, 1);
                try
                {
                    RenderControl(c);
                }
                finally
                {
                    c.Opacity = alpha;
                    c.Transformation = transformation;
                    c.Location = location;
                }
                Texture result = Capture(c, c.X, c.Y);
                Invalidated = true;
                Dirty = true;
                return result;
            }
            else return null;
        }

        #endregion

        public void Change(Rectangle rectangle)
        {
            this.ClientRectangle = rectangle;
            Invalidated = true;
        }

        /// <summary>
        /// Gets the rectangle for the currently rendering control.
        /// </summary>
        /// <returns></returns>
        public Rectangle GetCurrentBounds()
        {
            Rectangle rect = CurrentRenderData.ScissorBox;
            rect.Offset((int)CurrentRenderData.X, (int)CurrentRenderData.Y);
            return rect;
        }
    }
}
